/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */
package com.ucjava.chat.commserve.rmi.server;

import com.ucjava.chat.commserve.rmi.common.IChatClientRMI;
import com.ucjava.chat.commserve.rmi.common.Message;
import com.ucjava.chat.commserve.rmi.common.PrefStore;
import com.ucjava.chat.commserve.rmi.common.PrivacySettings;
import com.ucjava.chat.commserve.rmi.common.ResultCodes;
import com.ucjava.chat.commserve.rmi.common.User;
import java.rmi.RemoteException;
import java.sql.Timestamp;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 *
 * @author tony
 */
public class ClientHandler
{

    private static final String ONLY_PASSWORD = "pa$$w0rd";
    private static final ArrayList<String> validUsers =
            new ArrayList<String>(Arrays.asList(
            "alan", "andrew", "brandan",
            "jond", "jonk", "martin", "max",
            "sagrario", "sanju", "tony"));
    // Keep a list of currently connected clients. Use
    // of a Map here will ensure unique constraint.
    private Map<String, LoggedInClient> connectedClients =
            new HashMap<String, LoggedInClient>();
    private PrefStore prefStore;

    /**
     * Constructor Create and start threads to clean up inactive clients and
     * broadcast messages
     */
    public ClientHandler()
    {
        // Get a copy of the prefs/configure object:
        prefStore = PrefStore.makePrefStore();
        // Make an object to remove users
        // after specified period of inactivity:
        InactiveUser removeUser = new InactiveUser();
        // Make an associated thread and start it going:
        Thread removeUserThread = new Thread(removeUser, "Inactive User Cleanup Thread");
        removeUserThread.start();
        // Now an object to send messages out to the connected clients:
        MessageBroadcaster msgBroadcaster = new MessageBroadcaster();
        // and an associated thread:
        Thread msgBroadcastThread = new Thread(msgBroadcaster, "MessageBroadcasterThread");
        msgBroadcastThread.start();
    }

    /**
     * When MessageHandler hands a message off to us, check to see for whom it's
     * intended. If no users are specified by client that sent this message,
     * send it to all logged in clients. Then, for each intended recipient: If
     * that User is in this.connectedClients Put this message in that client's
     * queue
     */
    public void dispatchMessage(Message msg)
    {
        // Initialize msg recipients if not
        // already done. If no recipients
        // are specified, set the recipients
        // list to everyone who's logged in:
        List<User> uList = msg.getRecipients();
        if (uList == null || uList.isEmpty())
          {
            uList = this.getLoggedInUsers();
          }
        // Now enqueue message for each user in uList
        // who's currently logged in:
        for (User u : uList)
          {
            LoggedInClient cl = this.clientForName(u.getUserName());
            if (cl != null)
              {
                cl.enqueueMessage(msg);
              }
          }
    } // end dispatchMessage()

    /**
     * Return a list of LoggedInClient objects:
     *
     *
     * @return
     */
    public ArrayList<LoggedInClient> getLoggedInClients()
    {
        ArrayList<LoggedInClient> cList = new ArrayList<LoggedInClient>();
        for (String s : connectedClients.keySet())
          {
            LoggedInClient cl = connectedClients.get(s);
            try
              {
                if (cl.getRcvMsgStub().getPrivacySetting()
                        == PrivacySettings.OPEN)
                  {
                    cList.add(connectedClients.get(s));
                  }
              } catch (RemoteException ex)
              {
                Logger.getLogger(ClientHandler.class.getName()).log(Level.SEVERE, null, ex);
              }
          }
        return cList;
    }

    /**
     * Return a list of User objects corresponding to the logged-in users.
     * Only returns those with a privacy setting of OPEN.
     */
    public ArrayList<User> getLoggedInUsers()
    {
        ArrayList<User> uList = new ArrayList<User>();
        for(LoggedInClient cl : getLoggedInClients() )
          {
            User u = new User();
            u.setUserName(cl.getUsername());
            uList.add(u);
          }
        return uList;
    }

    ///////////// OTHER PUBLIC METHODS ////////////////////////
    /**
     * If this is a valid user, with the correct password, and not already
     * logged in, add the user to connectedClients and return success code.
     * Otherwise, return appropriate error code.
     *
     *
     * @param username
     * @param password
     * @param host -- ip address from which this client is logging in
     * @param rcvMsgStub -- RMI stub the server can use to call methods on the
     * client
     * @return
     */
    public ResultCodes doLogin(String username, String password, String host,
            IChatClientRMI rcvMsgStub)
    {
        // Is this user on the list of valid users?
        if (!isValidUser(username))
          {
            return ResultCodes.NO_SUCH_USER;
          }
        // Was the password passed OK?
        if (!passwordOK(username, password))
          {
            return ResultCodes.WRONG_PASSWORD;
          }
        // Is this user already logged in?
        if (isLoggedIn(username))
          {
            return ResultCodes.ALREADY_LOGGED_IN;
          }
        // Tests passed -- create a LoggedInClient object
        // and add it to the list.
        LoggedInClient cc = new LoggedInClient();
        cc.setHost(host);
        cc.setUsername(username);
        cc.updateAccessTime();
        cc.setRcvMsgStub(rcvMsgStub);
        synchronized (this)
          {
            connectedClients.put(username, cc);
          }
        return ResultCodes.LOGIN_OK;
    }

    /**
     * If this user is logged in, remove the corresponding entry from
     * connectedClients and return success code. Otherwise, return error code.
     *
     * @param username
     * @return
     */
    public synchronized ResultCodes doLogout(String username)
    {
        if (!isLoggedIn(username))
          {
            return ResultCodes.NOT_LOGGED_IN;
          }
        connectedClients.remove(username);
        return ResultCodes.LOGOUT_OK;
    }

    /**
     * Is this user already logged in?
     *
     * @param username
     * @return
     */
    public synchronized boolean isLoggedIn(String username)
    {
        return connectedClients.containsKey(username);
    }

    /**
     * If client by this name is logged in, return the LoggedInClient object
     * corresponding to that name. Otherwise, return null.
     *
     * @param username
     * @return
     */
    public synchronized LoggedInClient clientForName(String username)
    {
        return this.connectedClients.get(username);
    }

    /////////////PRIVATE METHODS////////////////////////
    /**
     * Is this user in the list of valid users? The list is static/final, so no
     * need to synchronize this.
     *
     * @param username
     * @return
     */
    private boolean isValidUser(String username)
    {
        return validUsers.contains(username);
    }

    /**
     * All this does is check the password against the universal password this
     * test server uses for all clients. In a production environment this would
     * probably use a database. The string is static/final, so no need to
     * synchronize this. That will change in a production environment.
     *
     * @param username
     * @param password
     * @return
     */
    private boolean passwordOK(String username, String password)
    {
        return password.equals(ONLY_PASSWORD);
    }

    /**
     * Meant to periodically send out messages in each logged in client's
     * sending queue.
     */
    public class MessageBroadcaster implements Runnable
    {
        // Loops forever. Each time through, calls sendMyMessages() for
        // each logged in client. If a RemoteException gets raised by that
        // call, that probably means there's a network error or that client
        // has logged off.
        //
        // Sleep interval between broadcasts can be tuned by using
        // prefStore.setMsgBroadcastInterval(interval) where
        // interval is the time in millisceconds.
        //

        @Override
        public void run()
        {
            try
              { // outer try -- for Thread.sleep()
                while (true)
                  {

                    for (LoggedInClient cl : ClientHandler.this.getLoggedInClients())
                      {
                        try // inner try -- for each client
                          { // because sendMyMessages() throws RemoteException
                            cl.sendMyMessages();
                          } catch (RemoteException ex)
                          { // Log the event, and then log out this client
                            Logger.getLogger(ClientHandler.class.getName()).log(Level.SEVERE, null, ex);
                            ClientHandler.this.doLogout(cl.getUsername());
                          } // end inner catch
                      } // end foreach client
                    Thread.sleep(prefStore.getMsgBroadcastInterval());
                  } // end while(true)
              } catch (InterruptedException ie) // possibly thrown during sleep()
              {
              } // end outer catch
        } // end run()  
    } // end MessageBroadcaster class

    /**
     * TODO: Restructure run() method so that it loops, and so that it acts on
     * each logged in client.
     */
    public class InactiveUser implements Runnable
    {
        // number of milliseconds in 30 minutes

        private static final long ACTIVITY_INTERVAL = 1800 * 1000;
        // How often to check -- milliseconds in 10 minutes:
        private static final long CHECK_INTERVAL = 600 * 1000;

        /**
         * Check if this client's last activity occurred more than
         * ACTIVITY_INTERVAL ago.
         *
         * This should probably be a method in the LoggedInClient class.
         *
         * @param cl
         * @return
         */
        private boolean isClientInactive(LoggedInClient cl)
        {
            Timestamp cutoff = new Timestamp(
                    (new Date()).getTime() - prefStore.getClientInactivityAllowance());
            //(new Date()).getTime() - ACTIVITY_INTERVAL);
            if (cl.getLastActivity().before(cutoff))
              {
                return true;
              } else
              {
                return false;
              }
        }

        @Override
        public void run()
        {
            while (true)
              {
                try
                  {
                    Iterator<LoggedInClient> iter =
                            ClientHandler.this.getLoggedInClients().iterator();
                    while (iter.hasNext())
                      {
                        LoggedInClient cl = iter.next();
                        if (isClientInactive(cl))
                          {
                            ClientHandler.this.doLogout(cl.getUsername());
                          }
                      }
                    Thread.sleep(prefStore.getClientCheckInterval());
//                    Thread.sleep(CHECK_INTERVAL);
                  } // end try
                catch (InterruptedException ex)
                  {
                  } // end catch
              }
        }
    }

}
